package com.qyh.hhh;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
public class ToggleMethodInterceptor implements MethodInterceptor {
    private final ApplicationContext applicationContext;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Toggle toggle = this.getAnnotation(invocation, Toggle.class);
        if (toggle != null) {
            On[] ons = toggle.ons();

            List<On> matches = Arrays.stream(ons)
                    .filter(on -> {
                        String expression = on.expression();

                        AutowireCapableBeanFactory bf = this.applicationContext.getAutowireCapableBeanFactory();

                        StandardBeanExpressionResolver resolver = new StandardBeanExpressionResolver(this.applicationContext.getClassLoader());

                        ConfigurableListableBeanFactory cbf = (ConfigurableListableBeanFactory) bf;
                        BeanExpressionContext exprContext = new BeanExpressionContext(cbf, null);

                        String value = cbf.resolveEmbeddedValue(expression);
                        if (value != null) {
                            Object evaluated = resolver.evaluate(value, exprContext);
                            value = (evaluated != null ? evaluated.toString() : null);
                        }
                        return "true".equalsIgnoreCase(value);
                    }).collect(Collectors.toList());

            // 没有一个命中, 调用默认的
            if (CollectionUtils.isEmpty(matches)) {
                Class<?> defaultClazz = toggle.defaultClazz();
                String defaultBeanName = toggle.defaultBeanName();

                return this.invoke(
                        invocation,
                        defaultClazz,
                        defaultBeanName,
                        "No expression matches"
                );
            }

            // 多个条件满足
            if (matches.size() > 1) {
                log.error("There are multi expression Can match, but you Don't allow it. Matched expressions are : {}."
                                + "Please check your expression and make sure there is only one expression Can be matched.",
                        matches.stream()
                                .map(On::expression)
                                .collect(Collectors.toList()));
                throw new IllegalArgumentException("There are multi expression Can match, but you Don't allow it");
            }
            On match = matches.get(0);
            Class<?> clazz = match.clazz();
            String beanName = match.beanName();
            String expression = match.expression();

            return this.invoke(
                    invocation,
                    clazz,
                    beanName,
                    "Expression : '{}' matches.",
                    expression
            );
        } else {
            // 没有注解的情况
            return invocation.proceed();
        }
    }

    private Object invoke(MethodInvocation invocation,
                          Class<?> clazz,
                          String beanName,
                          String msg,
                          Object... os) throws InvocationTargetException, IllegalAccessException {
        Method method = invocation.getMethod();
        Object[] args = invocation.getArguments();
        BeanDef beanDef = this.getBeanDef(clazz, beanName);

        Object targetBean = beanDef.getTargetBean();

        List<Object> objects = Arrays.asList(os);

        List<Object> list = new ArrayList<>(objects);

        list.add(targetBean.getClass().getName());
        list.add(method.getName());
        list.add(method.getParameterTypes());

        log.info(msg + "Call {}.{}.({})", list.toArray());

        return method.invoke(targetBean, args);
    }

    private BeanDef getBeanDef(Class<?> clazz, String beanName) {
        Class<?>[] interfaces = clazz.getInterfaces();

        List<? extends Map<String, ?>> collect = Arrays.stream(interfaces)
                .map(this.applicationContext::getBeansOfType)
                .collect(Collectors.toList());

        Set<BeanDef> defs = collect.stream()
                .map(map -> {
                    Set<String> keys = map.keySet();
                    return keys.stream()
                            .map(beanName1 -> {
                                Object bean = map.get(beanName1);
                                Object targetBean = this.getTargetBean(bean);
                                return new BeanDef(beanName1, bean, targetBean);
                            }).collect(Collectors.toSet());
                })
                .flatMap(Set::stream)
                .collect(Collectors.toSet());

        List<BeanDef> typeMatchBeansDef = defs.stream()
                .filter(def -> {
                    Object targetBean = def.getTargetBean();
                    return Objects.equals(targetBean.getClass().getName(), clazz.getName());
                }).collect(Collectors.toList());

        List<BeanDef> foundedBeans;
        if (!StringUtils.isEmpty(beanName)) {
            foundedBeans = typeMatchBeansDef
                    .stream()
                    .filter(beanDef -> beanDef.getName().equals(beanName))
                    .collect(Collectors.toList());
        } else {
            foundedBeans = typeMatchBeansDef;
        }

        if (CollectionUtils.isEmpty(foundedBeans)) {
            log.error("No bean could be found in spring. class : {}, beanName : {}", clazz, beanName);
            throw new IllegalArgumentException("No bean matched Could be found in spring.");
        }

        if (foundedBeans.size() > 1) {
            log.error("More than one bean Could be found in spring. clazz = {}, beanName = {}, foundedBeans = {}", clazz, beanName, foundedBeans);
            throw new IllegalArgumentException("More than one bean Could found in spring");
        }

        return foundedBeans.get(0);
    }

    private Object getTargetBean(Object bean) {
        try {
            Object target;
            if (bean instanceof Advised) {
                target = ((Advised) bean).getTargetSource().getTarget();
            } else {
                target = bean;
            }
            return target;
        } catch (Exception e) {
            log.error("get TargetBean error happens , bean = {}", bean, e);
            throw new RuntimeException(e);
        }
    }

    private <T extends Annotation> T getAnnotation(MethodInvocation invocation, Class<T> annotationType) {
        // 方法上面直接有注解
        if (AnnotatedElementUtils.hasAnnotation(invocation.getMethod(), annotationType)) {
            return AnnotatedElementUtils.findMergedAnnotation(invocation.getMethod(), annotationType);
        }
        // 从接口方法上拿
        Class<?> currentInterface = invocation.getMethod().getDeclaringClass();
        if (AnnotatedElementUtils.hasAnnotation(currentInterface, annotationType)) {
            return AnnotatedElementUtils.findMergedAnnotation(currentInterface, annotationType);
        }
        // 从实现类上面拿
        Class<?> currentImplementation = this.getTargetClass(invocation);
        if (AnnotatedElementUtils.hasAnnotation(currentImplementation, annotationType)) {
            return AnnotatedElementUtils.findMergedAnnotation(currentImplementation, annotationType);
        }
        // 找不到
        return null;
    }

    private Class<?> getTargetClass(MethodInvocation invocation) {
        Class<?> targetClass = null;
        Object thus = invocation.getThis();
        if (thus != null) {
            targetClass = AopUtils.getTargetClass(thus);
        }
        if (targetClass == null) {
            log.error("The target class is null, please check. invocation = {}", invocation);
            throw new IllegalArgumentException("the target class is null, please check.");
        }
        return targetClass;
    }
}
